/**
 * Created by wm123 on 2017/6/20.
 */
const server = require('server');
const extend = require('extend');
const Player = require('./player');
const Room = require('./room');
const Role = require('./role');
const Game = require('./game');
const Board = require('./board');
const Team = require('./team');
const Group = require('./group');
const Messages = require('./messages');
const Dispatcher = require('./dispatcher');
const utils = require('./utils');
const pack = require('./pack');
const { get, socket, error } = server.router;
const { file, status, send } = server.reply;
const path = require('path');
const seo = require('./seo');
const random = require('./random');

const execute = execute => data => {
    try {
        execute(data)
    } catch (e) {
        e && console.error(e);
        data.socket.emit('err', e && (e.message || e));
    }
};
const notEmpty = value => {
    return Array.isArray(value) ? value.some(o => typeof o === 'object' ? notEmpty(o) : typeof o !== 'undefined') : (typeof value !== 'undefined' && Object.keys(value).length > 0);
}
const notSameArray = (value, old, diff) => {
    if (Array.isArray(value) && Array.isArray(old) && Array.isArray(diff)) {
        if (diff.every(o => typeof o !== 'undefined' && typeof o !== 'object')) {
            return value.length !== old.length || value.some((o, index) => o !== old[index]);
        } else {
            return true;
        }
    } else {
        return true;
    }
}
const diff = (data, old) => {
    let isArray = Array.isArray(data);
    let result = (isArray ? [] : { });
    old = typeof old === 'undefined' ? (isArray ? [] : { }) : old;
    for (let name in data) {
        if (data.hasOwnProperty(name)) {
            let value = data[name];
            if (typeof value === 'undefined') {
                if (typeof old[name] !== 'undefined') {
                    if (isArray) {
                        result[name] = undefined;
                    } else {
                        result['~' + name] = true;
                    }
                }
            } else if (typeof value === 'object') {
                if (typeof old[name] === 'undefined') {
                    result[name] = value;
                } else {
                    let d = diff(value, old[name]);
                    if (isArray || notEmpty(d)) {
                        if (!Array.isArray(d) || notSameArray(value, old[name], d)) {
                            result[name] = d;
                        }
                    }
                }
            } else if (isArray || value !== old[name]) {
                result[name] = value;
            }
        }
    }
    if (typeof data !== 'undefined') {
        for (let name in old) {
            if (old.hasOwnProperty(name)) {
                if (!(name in data) && !name.startsWith('~')) {
                    result['~' + name] = true;
                }
            }
        }
    }
    return result;
};
const emit = (socket, event, data, key) => {
    if (key === false) {
        socket.emit(event, data);
    } else {
        let emits = socket.emits || { };
        let prop = typeof key !== 'undefined' ? event + '.' + data[key] : event;
        let send = diff(data, emits[prop]);
        if (notEmpty(send)) {
            socket.emit(event, send);
            emits[prop] = data;
            socket.emits = emits;
        }
    }
};
const getRemoteAddress = socket => socket.client.conn.request.headers['x-real-ip'] || socket.client.conn.remoteAddress;

const proxy = socket => new Proxy({ }, {
    get(target, property, receiver) {
        if (property in target) {
            return Reflect.get(target, property, receiver);
        } else {
            return data => emit(socket, property.replace(/^[_$]/i, ''), data, false);
        }
    }
});

const last = data => {
    if (Array.isArray(data)) {
        return [ data[data.length - 1] ];
    } else {
        if ('last' in data) {
            let last = data.last;
            if (typeof last === 'function') {
                return last.call(data);
            } else {
                return last;
            }
        } else if ('data' in data) {
            return last(data.data);
        }
    }
};

const apply = (execute, data, socket, server) => {
    if (execute) {
        if (Array.isArray(execute)) {
            execute.forEach(exec => apply(exec, data, socket, server));
        } else {
            execute({
                data, server, socket,
                emit: proxy(socket),
                player: socket.player,
                room: socket.player && socket.player.room,
                role: socket.player && socket.player.room && socket.player.role,
                game: socket.player && socket.player.room && socket.player.room.game,
                watch: (dispatcher, event, getter, key) => dispatcher && dispatcher.on(dispatcher => emit(socket, event, getter ? getter(dispatcher) : dispatcher.data, key), socket.player),
                append: (dispatcher, event, getter) => dispatcher && dispatcher.on(dispatcher => {
                    let data = last(getter ? getter(dispatcher) : dispatcher);
                    if (data) emit(socket, event, data, false);
                }, socket.player, true)
            });
        }
    }
};

const watchRole = (watch, role) => watch(role, 'role');
const watchPlayer = (watch, player) => watch(player, 'player');
const watchRoom = (watch, room) => watch(room, 'room');
const watchGame = (watch, game) => watch(game, 'game', game => game.summary);
const watchGroups = (watch, game) => game && game.groups.forEach(group => watch(group, 'group', group => group.summary, 'index'));
const watchBoard = (watch, game) => game && watch(game.board, 'board');
const appendRoomMessage = (append, room) => room && append(room.messages, 'message');
const appendGameMessage = (append, game) => game && append(game.messages, 'message');

const defaultMappings = {
    connection: () => { },
    login: [
        ({ data, socket, server }) => socket.player = Player.login(server, data.id, data.name || '', data.password),
        ({ watch, append, player, role, room, game }) => {
            watchPlayer(watch, player);
            watchRole(watch, role);
            watchRoom(watch, room);
            watchGame(watch, game);
            watchGroups(watch, game);
            watchBoard(watch, game);
            appendRoomMessage(append, room);
            appendGameMessage(append, game);
        }
    ],
    info: ({ data, socket, server }) => socket.player = Player.get(server, data.id, data.name || ''),
    rooms: ({ data, emit, server }) => {
        let page = data && data.page || 0;
        emit.rooms({ page, list: Room.list(server, page).map(room => room.summary) });
    },
    settings: ({ emit, server }) => { emit.settings(extend(true, { publish: true }, server.config.room)) },
    create: [
        ({ data, player }) => player.createRoom(data),
        ({ watch, append, role, room }) => {
            watchRole(watch, role);
            watchRoom(watch, room);
            appendRoomMessage(append, room);
        }
    ],
    join: [
        ({ data, player }) => player.joinRoom(data.roomId),
        ({ watch, append, role, room }) => {
            watchRole(watch, role);
            watchRoom(watch, room);
            appendRoomMessage(append, room);
        }
    ],
    exit: ({ role }) => role.exit(),
    move: ({ data, role }) => role.move(data),
    disable: ({ data, role }) => role.disable(data),
    enable: ({ data, role }) => role.enable(data),
    kick: ({ data, role }) => role.kick(data),
    start: ({ room, role }) => room.start(role),
    ready: [
        ({ role }) => role.ready(),
        ({ watch, append, game }) => {
            watchGame(watch, game);
            watchGroups(watch, game);
            watchBoard(watch, game);
            appendGameMessage(append, game);
        }
    ],
    confirm: ({ role }) => role.confirm(),
    message: ({ role, data }) => role.message(data),
    disconnect: ({ socket }) => {
        socket.player && socket.player.leave();
        socket.emits = { };
    }
};

class GameServer {
    /**
     *
     * @param mappings a list of functions, each function accept one param: <object> context.
     *        context.role: role in session
     *        context.data: the data received from client
     *        context.room: room of role in session
     *        context.server: the server object
     *        context.socket: the socket object which is connect to client
     *        context.game: game of role in session
     * @param config
     */
    constructor(mappings, ...config) {
        this.mappings = mappings;
        this.config = extend(true, { }, {
            Player, Room, Role, Game, Board, Team, Group,
            assets: './assets',
            compress: true,
            watch: false,
            devtool: 'source-map',
            stats: 'error-only',
            presets: [
                require.resolve('babel-preset-es2015'),
                require.resolve('babel-preset-stage-3')
            ],
            mvvm: 'vue',
            css: 'less',
            random: random.car
        }, {
            teams: [
                {
                    color: 'white'
                }
            ],
            team: {
                size: 5,
                min: 2,
                regroup: false
            }
        }, ...config);
        this.players = new Map();
        this.rooms = new Map();
        this.roomIds = [];
    }
    start() {
        pack(this.config, () => {
            if (!this.server) {
                this.server = server({ port: this.config.port, 'public': this.config.assets, views: path.join(process.cwd(), this.config.assets), index: false },
                    [
                        get('/', ctx => {
                            return seo.need(ctx.req) ? file(path.join(this.config.assets, 'seo.html')) : file(path.join(this.config.assets, 'entry.html'));
                        }),
                        get('/id', ctx => ctx.req.session.id),
                        get('/random', async ctx => send(await this.config.random(ctx)))
                    ]
                        .concat(Object.keys(defaultMappings).map(key => {
                            socket(key, execute(({ data, socket }) => {
                                apply(defaultMappings[key], data, socket, this);
                                apply(this.mappings[key], data, socket, this);
                            }))
                        }))
                        .concat(Object.keys(this.mappings).filter(key => !(key in defaultMappings)).map(key => socket(key, execute(({ data, socket }) => apply(this.mappings[key], data, socket, this)))))
                        .concat([
                            error(ctx => status(500).send(ctx.error.message))
                        ]));
                console.log(`${ this.config.name || '' }服务器在端口${ this.config.port }上已启动`);
            }
        })
    }
    static start(mappings, ...config) {
        return new GameServer(mappings, ...config).start();
    }
}

module.exports = {
    GameServer, Player, Room, Role, Game, Board, Team, Group, Messages, Dispatcher, utils, random
};